﻿// (c) Copyright 2013 Mike Riedel.
// This source is subject to the Microsoft Public License (Ms-PL).
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;

namespace FreshMeat.Storage
{
   /// <summary>
   /// Provides file naming and hashing.
   /// </summary>
   /// <typeparam name="TId">Type of ID.</typeparam>
   public class FileLocator<TId>
   {
      const string TempExtension = ".tmp";
      const string BaseFolder = "";
      readonly IFileSystem fileSystem;
      readonly string extension;
      readonly Func<string, TId> idParser;
      readonly string searchPattern;
      int nextTempId;

      /// <summary>
      /// Creates instance using specified parameters.
      /// </summary>
      /// <param name="fileSystem">File system to use.</param>
      /// <param name="isHashed">True to use hashing to organize files into subfolders to avoid performance penalties
      /// associated with having too many files in a single folder; false to use a single folder.</param>
      /// <param name="extension">Extension for file names or null for default.</param>
      /// <param name="idParser">Converts a string to a <typeparamref name="TId"/> or null to use <see cref="Convert.ChangeType"/>.</param>
      public FileLocator(IFileSystem fileSystem, bool isHashed = false, string extension = null, Func<string, TId> idParser = null)
      {
         if (fileSystem == null)
            throw new ArgumentNullException("fileSystem");
         this.fileSystem = fileSystem;

         UsesSubfolders = isHashed;

         if (extension == null)
            extension = ".xml";
         if (extension.Length == 0 || (extension[0] == Path.ExtensionChar && extension.Length == 1))
            throw new ArgumentOutOfRangeException("extension", string.Format("Cannot be null or empty or just a '{0}'.", Path.ExtensionChar));
         if (extension[0] != Path.ExtensionChar)
            extension = Path.ExtensionChar + extension;
         this.extension = extension;

         this.idParser = idParser;

         searchPattern = '*' + extension;

         fileSystem.CreateFolder(BaseFolder);

         nextTempId = ManageTemps();
      }

      int ManageTemps()
      {
         // Find all existing temp files
         var temps = fileSystem.EnumerateFiles(BaseFolder, "*" + TempExtension, UsesSubfolders);

         // Find the next available id and then delete the files.
         // Deleted files are not available until next time!
         var max = -1;
         foreach (var temp in temps)
         {
            var id = int.Parse(Path.GetNameWithoutExtensions(temp));
            if (id > max)
               max = id;

            fileSystem.DeleteFile(temp);
         }

         // Return next available ID
         return max + 1;
      }

      /// <summary>
      /// Gets a unique temp name based on the specified file path.
      /// </summary>
      /// <param name="filePath">Path to a file.</param>
      /// <returns>Path to a temp file in the same folder.</returns>
      public string GetTempName(string filePath)
      {
         var temp = Path.Rename(filePath, nextTempId + TempExtension);
         nextTempId++;

         return temp;
      }

      /// <summary>
      /// Gets the path to the file associated with an ID.
      /// Derived classes can override this to customize path usage.
      /// </summary>
      /// <param name="id">ID for which to generate the path.</param>
      /// <returns>Path to file.</returns>
      public virtual string GetPath(TId id)
      {
         var name = id + extension;
         var path = Path.Append(BaseFolder, name);
         return path;
      }

      /// <summary>
      /// Gets the ID from a path.
      /// Derived classes can override this to customize path usage.
      /// </summary>
      /// <param name="path">Path generated by <see cref="GetPath"/>.</param>
      /// <returns>The ID associated with the path.</returns>
      public virtual TId GetId(string path)
      {
         var nameWithoutExtension = Path.GetNameWithoutExtensions(path);

         if (idParser != null)
            return idParser(nameWithoutExtension);

         return (TId)Convert.ChangeType(nameWithoutExtension, typeof(TId), CultureInfo.InvariantCulture);
      }

      /// <summary>
      /// Enumerates all data files.
      /// </summary>
      /// <returns>Paths to data files.</returns>
      public IEnumerable<string> EnumerateFiles()
      {
         return fileSystem.EnumerateFiles(BaseFolder, searchPattern, UsesSubfolders);
      }

      /// <summary>
      /// Enumerates all data files.
      /// </summary>
      /// <returns>Paths to data files.</returns>
      public Task<IEnumerable<string>> EnumerateFilesAsync()
      {
         return fileSystem.EnumerateFilesAsync(BaseFolder, searchPattern, UsesSubfolders);
      }

      /// <summary>
      /// File system.
      /// </summary>
      public IFileSystem FileSystem { get { return fileSystem; } }

      /// <summary>
      /// Extension for files.
      /// </summary>
      public string Extension { get { return extension; } }

      /// <summary>
      /// Indicates whether this locator uses subfolders.
      /// </summary>
      public bool UsesSubfolders { get; private set; }
   }
}